[WIP][POC] Pgaur on pair evaluation - #50791
Draft
prtkgaur wants to merge 24 commits into
Draft
Conversation
A C++ implementation of the OnPair short-string codec (arXiv:2508.02280): a trained dictionary of up to 16-byte tokens, greedy longest-prefix tokenization, and a branch-free gather-copy decode that keeps per-row random access. The dictionary budget is configurable from 9 to 16 bits; codes are bit-packed at the dictionary's true width.
Splits each value into a shared prefix and a suffix before the symbol table sees it, following the FSST+ thesis (Alexandre, CWI 2025). Used to measure whether prefix extraction adds anything on top of either codec.
Measures FSST, OnPair, the prefix-extraction variants and zstd/lz4 pages on one corpus set, on three axes: compression ratio, whole-column decode with per-row random access, and encode. Every codec is charged a bit-packed per-row length array so column reconstruction costs the same across all of them. bench_common.h holds the timing and bit-packing helpers shared with the cascade benchmark. The Rust helper generates the corpora: TPC-H string columns, the OnPair paper's real-world datasets, ClickBench columns, and synthetic identifier and JSON sets.
Measures the native Parquet pages as libparquet writes them, each on its own and followed by zstd(1) or lz4, against FSST and OnPair with and without a generic codec on top. Also measures the dictionary-then-OnPair cascade, which dictionary-encodes the column and OnPairs only the distinct values. Native pages are charged only what the writer emits, since they carry their own lengths; the candidate codecs are charged a separate length array. Auto-budget selection runs in per-corpus setup, outside the timed region, as every codec's parameter choice does.
The roundtrip checker now runs every dictionary budget from 9 to 16 rather than only 16, for both the plain and the dedup layout, and exits non-zero on any mismatch so it can gate a benchmark run. The packed decode loop is templated on the code width and the auto budget picks a width per column, so checking only 16 left the width the benchmarks report unverified. The width sweep answers whether a wider code decodes faster without the confound a budget sweep carries. It holds one trained dictionary and its code stream fixed and re-packs the same codes at every width up to 16, so tokens, token count and copy width are identical and only the unpacking differs.
|
Thanks for opening a pull request! This pull request has been automatically converted to a draft because its title doesn't match Arrow's required format. If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project. Then could you also rename the pull request title in the following format? or After updating the title, you can mark the pull request as ready for review. See also: |
The comment above the decode loop claimed it was store-bandwidth-bound, on the evidence that (over-copy factor) x (decode MiB/s) was constant across corpora. That product is equally constant when the loop is bound by tokens retired, since over-copy is kCopy/mean_token_len and throughput is mean_token_len x tokens/s -- the two models are indistinguishable from that measurement, and the wrong one was picked. It then steered two experiments (block-wise unpacking with prefetch, a narrower two-store copy) at a cost that was not the constraint. An ablation ladder separates them. Deleting the stores makes the loop slower; deleting the random dictionary read nearly doubles it. The cost is the gather: the stored layout reads a u32 offsets pair from one array and the payload from a variable-stride blob, so every token touches two independent random cache lines. FSST's decoder never pays this -- it reads a fixed-stride symbol[] plus len[]. Add StridedDictionary, a decode-side view giving each token a fixed 16-byte slot and a length byte in a dense side array, so a token is one cache line. It is deliberately not the stored form: ~17 bytes per token against ~12 would add hundreds of KiB to a 65k-token dictionary and move every ratio. It is built once per column from an unchanged CompactDictionary and thrown away, costing nothing on disk. Skipped when the code stream is too short to amortise the O(tokens) build. Where a predicated store is available the loop writes exactly one token's length, which removes the fixed over-copy and collapses the 4/8/16 width dispatch to a single kernel per code width. The guard is compile-time on purpose: the portable path is byte-identical and only slower, so a build without it loses speed and nothing else. The strided load alone, with today's fixed-width store, is worth most of the win and needs no ISA feature. DecompressPacked keeps its signature and builds the view internally; a new overload takes a view the caller built once, for a reader decoding many pages against one dictionary. verify_roundtrip gains a byte-exact gate gating all four decode paths -- whole column, self-building packed, prebuilt view, and the short-stream fallback -- against a scalar reference, since nothing else reaches the last of those. Verified under -march=native and the portable fallback: 30 corpora, 86,400,060 row comparisons, 120 path gates, no disagreements under either. Compressed sizes and ratios are bit-identical to the published ladder run on all shared rows.
At a fixed code width the (byte offset, intra-byte shift) pair a packed code is read with repeats with a period of 8 / gcd(width, 8) codes. Unrolling the decode loop by a whole multiple of that period makes every offset and every shift a compile-time constant and advances the stream cursor once per group rather than once per code. The group size is a request that is rounded up to a whole period, so it is a floor rather than the group emitted. That is what lets one setting be constant-addressed at every width including 16, where the period is a single code and unrolling by the period alone would be a no-op -- which is exactly the high-cardinality half of the corpus set. Measured with the builds alternating many times over, pinned, order rotated and reversed between repetitions, every comparison a paired difference taken inside one repetition: +4.6% on the corpus-set median with the predicated store and +20.7% on the portable build, +34% on the two 14-bit columns where it removes a defect in the old loop rather than raising a ceiling. Nothing about the compressed form is touched, so no ratio moves. The verifier gains a sweep over sixteen consecutive stream lengths per column, which covers every remainder any group size up to sixteen can leave after its last whole group. A column whose code count is a whole multiple of the group never reaches the remainder path, so a whole-column check alone cannot see a fault there: with the remainder loop deliberately made to skip its first code, 43 of the 120 checkpoints are caught by the length sweep and by nothing else. An empty corpus is now a failure rather than a vacuous pass.
…Pair The reference FSST trainer cannot be widened in place. Its pair counter is a dense square over the code space, which is a few hundred kilobytes at nine bits and tens of gigabytes at sixteen, and its symbol type packs the bytes into a single machine word, capping a symbol at eight bytes. Both are replaced here, the counter by an open-addressed map over occupied pairs and the symbol by a fixed byte array with an explicit length, so the same iterative local search can fill a sixteen-bit table with symbols as long as OnPair's. The trained table is handed to OnPair's own encoder through a new train-free entry point, so the parsing pass and the decode kernel are shared and only the table construction differs. That makes the ratio and decode columns attributable to the table alone. The round-trip verifier now gates rather than reports, returning non-zero on any failure, and it decodes every FSST16 configuration twice, once through the whole-column path and once through the bit-packed path at the width the trained table needs, because a table of a few hundred tokens exercises a much narrower packed loop than OnPair ever reaches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thanks for opening a pull request!
If this is your first pull request you can find detailed information on how to contribute here:
Please remove this line and the above text before creating your pull request.
Rationale for this change
What changes are included in this PR?
Are these changes tested?
Are there any user-facing changes?
This PR includes breaking changes to public APIs. (If there are any breaking changes to public APIs, please explain which changes are breaking. If not, you can remove this.)
This PR contains a "Critical Fix". (If the changes fix either (a) a security vulnerability, (b) a bug that caused incorrect or invalid data to be produced, or (c) a bug that causes a crash (even when the API contract is upheld), please provide explanation. If not, you can remove this.)